업무 모듈 디렉토리 깊게 파헤치기

Areas, Controllers, Pages, Contracts

QCN

ASP.NET Core 공유 프레임워크 사용

  • .NET Core 3.0 이후 클래스 라이브러리에서 ASP.NET Core API 사용하도록 개선되었습니다.
  • Microsoft.NET.Sdk 또는 Microsoft.NET.Sdk.Razor SDK를 사용하는 프로젝트는 공유 프레임워크에서 ASP.NET Core API를 사용하려면 ASP.NET Core를 참조합니다.
  • MVC 확장성 포함 Razor 보기 또는 Razor 페이지 지원이 가능합니다.
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
QCN

Area란 무엇인가?

  • ASP.NET Core 에서 컨트롤러, 뷰, 모델을 논리적으로 구분해 영역별로 모듈화하기 위한 기능입니다.
  • Area 기능으로 모듈 간의 엔드포인트의 충돌을 근본적으로 방지합니다.
/{area}/{controller}/{action}/{id?}
/Areas
   /Admin
      /Controllers
         DashboardController.cs
      /Views
         /Dashboard
            Index.cshtml
      /Models
QCN

API의 심장: Controllers 디렉토리

  • 위치: Areas/[모듈 ID]/Controllers
  • 역할: 클라이언트(웹 브라우저 등)로부터 오는 API 요청을 받아 처리하는 로직이 담겨있습니다.
  • 파일: HomeController.cs, BoardController.cs 와 같은 C# 클래스 파일로 구성됩니다.
  • 주로 JSON 형태의 데이터를 주고받는 RESTful API 엔드포인트를 정의합니다.
/Areas
└── sample
    └── Controllers
        └── HomeController.cs
QCN

Controllers 예시 코드

  • /api/[모듈 ID]/home/hello 요청을 처리하는 간단한 예시입니다.
  • 다음 코드는 GET /api/sample/home/hello 요청 시 "Hello from Server!" 라는 텍스트를 반환합니다.
using Microsoft.AspNetCore.Mvc;

namespace sample.Areas.sample.Controllers
{
    [Area("sample")]
    [Route("[area]/api/[controller]")]
    public class HomeController : Controller
    {
        [HttpGet("hello")]
        public ActionResult<string> SayHello()
        {
            return "Hello from Server!";
        }
    }
}
QCN

서버가 그리는 화면: Pages 디렉토리

  • 위치: Areas/[모듈 ID]/Pages
  • 역할: 서버 측에서 렌더링되는 UI, 즉 사용자에게 보여질 화면(HTML)을 만듭니다.
  • 파일: 주로 Razor Pages 기술을 사용하며, Index.cshtml(View)과 Index.cshtml.cs(Code-behind) 파일이 하나의 쌍으로 동작합니다.
  • 서버의 데이터를 받아 동적인 웹 페이지를 생성할 때 사용됩니다.
/Areas
└── Sample
    └── Pages
        ├── Index.cshtml
        └── Index.cshtml.cs
QCN

Pages 파일의 역할

  • Index.cshtml

    • HTML 구조와 함께 C# 코드를 사용하여 서버 데이터를 화면에 표시합니다.
    • @ 기호를 사용하여 C# 변수나 로직을 HTML에 삽입합니다.
  • Index.cshtml.cs (코드 비하인드)

    • 페이지가 로드될 때 실행될 C# 로직을 담습니다.
    • 데이터베이스에서 데이터를 조회하거나, 클라이언트 요청을 처리하여 그 결과를 cshtml 파일로 전달합니다.
  • Razor 구문을 사용하는 ASP.NET 웹 프로그래밍 소개

QCN

계약의 정의: Contracts 디렉토리

  • 위치: [모듈 ID]/Contracts 화면/기능 개발자의 메인 디렉토리입니다.
  • 컴파일 할 때 모든 모듈의 환경 설정과 개발 소스는 프론트엔드와 백엔드 모두 $(HANDSTACK_HOME)/contracts 단일 디렉토리내에서 관리됩니다.
  • 텍스트 편집기 기반의 개발 도구로 컴파일 없이 앱 개발 가능합니다.
[모듈 ID]/contracts
├─command
├─dbclient
├─function
├─graphclient
├─prompter
├─repository
├─transact
└─wwwroot
QCN

contracts 디렉토리 구조

프로젝트 관리에 필요한 소스 운영 기준을 만들기 위해 디렉토리와 파일 명명에 대한 규칙을 다음과 같이 권장합니다.

module
   └─HDS: 기본 애플리케이션 ID 3자리
       └─TST: 프로젝트 ID 3자리
           └─TST010: 파일 ID 
QCN

어플리케이션 계약 디렉토리

HandStack에서 공식 제공되는 모든 디렉토리와 소스 코드 들은 이 규칙을 준수합니다.

%HANDSTACK_HOME%\contracts
├─command
│  └─HDS
│      └─TST
├─dbclient
│  └─HDS
│      └─TST
├─function
│  ├─csharp
│  │  └─HDS
│  │      └─TST
│  │          └─TST010
│  └─javascript
│      └─HDS
│          └─TST
│              └─TST020
├─graphclient
│  └─HDS
│      └─TST
├─prompter
│  └─HDS
│      └─TST
├─repository
│  └─HDS
├─transact
│  └─HDS
│      ├─STR
│      └─TST
└─wwwroot
   └─HDS
       └─TST
QCN

그외에 모듈 내 디렉토리 구조

  • 모듈의 가독성과 유지보수성을 위해 다음과 같은 디렉토리 구조를 권장합니다.

  • Entity: 데이터베이스 테이블과 매핑되는 모델 클래스

  • Events: 비동기 처리를 위한 이벤트 정의

  • Extensions: 기존 클래스를 확장하는 헬퍼 메서드

  • Services: 비즈니스 로직을 처리하는 서비스 클래스

  • Settings: 모듈별 환경 설정 값

QCN

HandStack의 ack 서버

  • ack 서버는 HandStack 애플리케이션의 시작점입니다.
  • 이 웹 서버가 시작될 때, 필요한 모듈들을 로딩하여 메모리에 올립니다.
  • 그 후, 클라이언트로부터 들어오는 모든 HTTP 요청(API, Page)을 각 모듈의 적절한 ControllerPage로 전달하고 응답을 처리하는 중심 역할을 수행합니다.
  • ack 서버의 appsettings.json 에 로드되는 모듈 정보를 확인하세요.
QCN

로딩되는 모듈 확인하기

  • ack 서버가 어떤 모듈을 로드하는지는 설정 파일에서 확인할 수 있습니다.
  • 아래 경로의 파일을 열어 LoadModules 항목을 살펴봅시다.
    • 경로: $(HANDSTACK_SRC)/1.WebHost/ack/appsettings.json
{
  // ... 생략 ...
  "LoadModules": [
      "wwwroot",
      "transact",
      "dbclient",
      "graphclient",
      "function",
      "command",
      "prompter",
      "repository",
      "logger",
      "checkup",
      "forwarder"
  ],
  // ... 생략 ...
}
QCN

checkup 모듈 설정 확인하기

{
    "ModuleID": "checkup",
    "Name": "checkup",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "ManagedAccessKey": "6eac215f2f5e495cad4f2abfdcad7644",
        "EncryptionAES256Key": "1234567890123456",
        "AdministratorEmailID": "administrator@handstack.kr",
        "ModuleConfigurationUrl": "http://localhost:8421/checkup/api/managed/initialize-settings",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "ModuleLogFilePath": "../log/checkup/module.log",
        "ModuleBasePath": "../modules/checkup",
        "WWWRootBasePath": "../modules/checkup/wwwroot/checkup",
        "EventAction": [
            "repository.Events.RepositoryRequest"
        ],
        "SubscribeAction": [],
        "ConnectionString": "URI=file:../sqlite/HDS/dbclient/checkup.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;"
    }
}
QCN

dbclient 모듈 설정 확인하기

dbclient 모듈 참고하기

{
    "ModuleID": "dbclient",
    "Name": "dbclient",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "CircuitBreakResetSecond": 60,
        "DefaultCommandTimeout": 30,
        "ContractBasePath": [
            "../contracts/dbclient"
        ],
        "IsTransactionLogging": false,
        "ModuleLogFilePath": "../log/dbclient/module.log",
        "IsLogServer": true,
        "LogServerUrl": "http://localhost:8421/logger/api/log/insert",
        "IsProfileLogging": false,
        "ProfileLogFilePath": "../log/dbclient/profile.log",
        "EventAction": [],
        "SubscribeAction": [
            "dbclient.Events.DbClientRequest",
            "dbclient.Events.ManagedRequest"
        ],
        "DataSource": [
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "CHECKUPDB",
                "DataProvider": "SQLite",
                "ConnectionString": "URI=file:../sqlite/HDS/dbclient/checkup.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;",
                "IsEncryption": "N",
                "Comment": "SQLite 기본 데이터베이스"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "DB01",
                "DataProvider": "SQLite",
                "ConnectionString": "URI=file:../sqlite/HDS/dbclient/HDS.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;",
                "IsEncryption": "N",
                "Comment": "SQLite 기본 데이터베이스"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "DB02",
                "DataProvider": "SqlServer",
                "ConnectionString": "Data Source=localhost;Initial Catalog=master;User ID=sa;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "Comment": "SqlServer 기본 데이터베이스"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "DB03",
                "DataProvider": "Oracle",
                "ConnectionString": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=ORCL)));User Id=system;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "Comment": "Oracle 기본 데이터베이스"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "DB04",
                "DataProvider": "MySQL",
                "ConnectionString": "Server=localhost;Port=3306;Uid=root;Pwd=Strong@Passw0rd;PersistSecurityInfo=True;SslMode=none;Charset=utf8;Allow User Variables=True;",
                "IsEncryption": "N",
                "Comment": "MySQL 기본 데이터베이스"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "DB05",
                "DataProvider": "PostgreSQL",
                "ConnectionString": "Host=localhost;Port=5432;Database=postgres;User ID=postgres;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "Comment": "PostgreSQL 기본 데이터베이스"
            }
        ]
    }
}
QCN

function 모듈 설정 확인하기

nuget, npm, pypi 패키지 관리자로 서버 함수에 필요한 패키지를 관리합니다.

function 모듈 참고하기

{
    "ModuleID": "function",
    "Name": "function",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "CircuitBreakResetSecond": 60,
        "IsLogServer": true,
        "LogServerUrl": "http://localhost:8421/logger/api/log/insert",
        "ContractBasePath": [
            "../contracts/function"
        ],
        "ModuleLogFilePath": "../log/function/module.log",
        "NodeFunctionConfig": {
            "LocalStoragePath": "../cache/function",
            "LogMinimumLevel": "trace",
            "FileLogBasePath": "../log/function/javascript",
            "TimeoutMS": -1,
            "IsSingleThread": true,
            "WatchGracefulShutdown": true,
            "EnableFileWatching": true,
            "WatchFileNamePatterns": [ "featureMain.js" ],
            "NodeAndV8Options": "",
            "EnvironmentVariables": ""
        },
        "CSharpFunctionConfig": {
            "EnableFileWatching": true,
            "FileLogBasePath": "../log/function/csharp",
            "WatchFileNamePatterns": [ "featureMain.cs" ],
        },
        "EventAction": [],
        "SubscribeAction": [],
        "FunctionSource": [
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "FN01",
                "DataProvider": "SQLite",
                "ConnectionString": "URI=file:../sqlite/HDS/dbclient/HDS.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;",
                "IsEncryption": "N",
                "WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN01",
                "Comment": "SQLite 기본 거래"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "FN02",
                "DataProvider": "SqlServer",
                "ConnectionString": "Data Source=localhost;Initial Catalog=master;User ID=sa;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN02",
                "Comment": "SqlServer 기본 거래"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "FN03",
                "DataProvider": "Oracle",
                "ConnectionString": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=ORCL)));User Id=system;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN03",
                "Comment": "Oracle 기본 거래"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "FN04",
                "DataProvider": "MySQL",
                "ConnectionString": "Server=localhost;Port=3306;Uid=root;Pwd=Strong@Passw0rd;PersistSecurityInfo=True;SslMode=none;Charset=utf8;Allow User Variables=True;",
                "IsEncryption": "N",
                "WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN04",
                "Comment": "MySQL 기본 거래"
            },
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "DataSourceID": "FN05",
                "DataProvider": "PostgreSQL",
                "ConnectionString": "Host=localhost;Port=5432;Database=postgres;User ID=postgres;Password=Strong@Passw0rd;",
                "IsEncryption": "N",
                "WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN05",
                "Comment": "PostgreSQL 기본 거래"
            }
        ]
    }
}
QCN

repository 모듈 설정 확인하기

repository 모듈 참고하기

{
    "ModuleID": "repository",
    "Name": "repository",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "FileServerUrl": "http://localhost:8421",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "ContractBasePath": [
            "../contracts/repository"
        ],
        "ModuleLogFilePath": "../log/repository/module.log",
        "DatabaseContractPath": "../contracts/dbclient",
        "ModuleBasePath": "../modules/repository",
        "EventAction": [],
        "SubscribeAction": [ "repository.Events.RepositoryRequest" ],
        "XFrameOptions": "ALLOW-FROM http://127.0.0.1:8421,http://localhost:8421",
        "ContentSecurityPolicy": "frame-ancestors 'self' http://127.0.0.1:8421 http://localhost:8421;"
    }
}
QCN

transact 모듈 설정 확인하기

transact 모듈 참고하기

{
    "ModuleID": "transact",
    "Name": "transact",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "CircuitBreakResetSecond": 60,
        "IsValidationRequest": false,
        "IsAllowDynamicRequest": true,
        "AllowTenantTransactionCommands": [ "D" ],
        "IsLogServer": true,
        "LogServerUrl": "http://localhost:8421/logger/api/log/insert",
        "IsTransactionLogging": true,
        "IsTransactAggregate": true,
        "IsDataMasking": false,
        "MaskingChar": "*",
        "MaskingMethod": "Syn",
        "ContractBasePath": [
            "../contracts/transact"
        ],
        "AvailableEnvironment": [ "P", "D", "S" ],
        "IsCodeDataCache": true,
        "CodeDataCacheTimeout": 20,
        "ModuleBasePath": "../modules/transact",
        "TransactionLogBasePath": "../sqlite/aggregate",
        "UseApiAuthorize": false,
        "BypassAuthorizeIP": [
            "localhost",
            "127.0.0.1"
        ],
        "AllowRequestTransactions": {
            "HDS": [ "*" ]
        },
        "RoutingCommandUri": {
            "HDS|*|D|D": "http://localhost:8421/dbclient/api/query",
            "HDS|*|G|D": "http://localhost:8421/graphclient/api/query",
            "HDS|*|F|D": "http://localhost:8421/function/api/execution",
            "HDS|*|C|D": "http://localhost:8421/command/api/execution",
            "HDS|*|P|D": "http://localhost:8421/prompter/api/query",
            "HDS|*|D|P": "http://localhost:8421/dbclient/api/query",
            "HDS|*|G|P": "http://localhost:8421/graphclient/api/query",
            "HDS|*|F|P": "http://localhost:8421/function/api/execution",
            "HDS|*|C|P": "http://localhost:8421/command/api/execution",
            "HDS|*|P|P": "http://localhost:8421/prompter/api/query",
            "HDS|*|D|T": "http://localhost:8421/dbclient/api/query",
            "HDS|*|G|T": "http://localhost:8421/graphclient/api/query",
            "HDS|*|F|T": "http://localhost:8421/function/api/execution",
            "HDS|*|C|T": "http://localhost:8421/command/api/execution",
            "HDS|*|P|T": "http://localhost:8421/prompter/api/query"
        },
        "EventAction": [],
        "SubscribeAction": [
            "transact.Events.TransactRequest"
        ],
        "PublicTransactions": [
            {
                "ApplicationID": "HDS",
                "ProjectID": "*",
                "TransactionID": "*"
            }
        ]
    }
}
QCN

logger 모듈 설정 확인하기

logger 모듈 참고하기

{
    "ModuleID": "logger",
    "Name": "logger",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "IsSQLiteCreateOnNotSettingRequest": true,
        "LogDeleteRepeatSecond": 43200,
        "ModuleBasePath": "../modules/logger",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "EventAction": [],
        "SubscribeAction": [],
        "DataSource": [
            {
                "ApplicationID": "HDS",
                "TableName": "TransactLog",
                "DataProvider": "SQLite",
                "RemovePeriod": -30,
                "ConnectionString": "URI=file:../sqlite/HDS/logger/transact.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;",
                "IsEncryption": "N"
            }
        ]
    }
}
QCN

wwwroot 모듈 설정 확인하기

wwwroot 모듈 참고하기

libman.json 으로 화면 개발에 필요한 오픈소스 관리합니다.

{
    "ModuleID": "wwwroot",
    "Name": "wwwroot",
    "IsBundledWithHost": false,
    "Version": "1.0.0",
    "ModuleConfig": {
        "SystemID": "HANDSTACK",
        "BusinessServerUrl": "http://localhost:8421/transact/api/transaction/execute",
        "ModuleLogFilePath": "../log/wwwroot/module.log",
        "ContractRequestPath": "view",
        "ContractBasePath": "../contracts/wwwroot",
        "WWWRootBasePath": "../modules/wwwroot/wwwroot"
    }
}
QCN

새로 추가된 기본 모듈 확인하기

현재 HandStack 기본 모듈에는 다음 확장 모듈도 포함됩니다.

모듈 역할
command 계약 기반 CLI 프로세스와 Web URL 호출 실행
graphclient Neo4j, Memgraph 기반 Cypher 실행
prompter LLM 프롬프트 계약 실행과 도구 호출 관리
forwarder Playwright 기반 세션 유지형 포워드 프록시

자세한 설정은 /docs/reference/api/modules/[모듈 ID] 문서를 참고하세요.

QCN

요약 정리 및 Q&A

  • HandStack은 여러분이 클라이언트와 서버에서 업무 화면과 기능을 개발 할 때 필요한 기본적인 틀과 오픈소스를 제공합니다.
  • 덕분에 개발자는 복잡한 설정이나 기반 구조에 쏟는 시간을 줄이고, 핵심 비즈니스 로직 개발에만 집중할 수 있죠!
QCN